classdef dotsAllRemoteObjectManagers < dotsAllObjectManagers
    % @class dotsAllRemoteObjectManagers
    % Superclass for singleton objects that coordinate remote machine
    % behaviors.
    % @details
    % dotsAllRemoteObjectManagers extends the dotsAllObjectManagers
    % superclass to coordinate between serparate instances of Matlab
    % and separate machines.  Each remote manager object can communicate
    % with its remote counterpart, which is a manager of the same class
    % running in a different Matab instance.  The counterpart can in turn
    % coordinate behaviors on behalf of its managed objects.
    % @details
    % Behaviors to coordinate remotely might include creation and deletion
    % of objects or specific object behaviors like drawing graphics or
    % playing sounds.
    % @details
    % dotsTheMessenger handles communications between remote manager
    % counterparts.  Comminications settings, like IP addresses and ports,
    % should be set up using dotsTheMachineConfiguration.
    % @details
    % Remote managers use the clientMode and serverMode shared properties
    % to guide their behaviors (see dotsTheSwitchboard for more shared
    % properties).  When clientMode is true, a manager will try to delegate
    % behaviors to its remote counterpart.  When serverMode is true, a
    % manager will try to carry out behaviors that were delegated to it.
    % @details
    % Depending on shared property values and communications settings,
    % remote managers may operate in four distinct ways:
    %   - <b>No Delegation</b>: if clientMode and serverMode are both inactive,
    %   remote managers will carry out behaviors themselves without sending
    %   messages.
    %   - <b>Self Delegation</b>: if clientMode and serverMode are both
    %   active, and communications settings all refer to the local machine,
    %   remote  managers will delegate behaviors to themselves.  They will
    %   send messages through dotsTheMessenger, but the behavior is
    %   expected to be the same as with "No Delegation".
    %   - <b>Client Delegation</b>: if clientMode is active, remote
    %   managers will delegate behaviors by sending messages through
    %   dotsTheMessengter to whatever machine is specified in the
    %   communictations settings.
    %   - <b>Server Compliance</b>: if serverMode is active, remote
    %   managers will carry out behaviors as requested in messages sent
    %   from a "clientMode" counterpart.
    %   .
    % "No Delegation" should produce the fastest behavior within a single
    % instance of Matlab.  "Self Delegation" is useful for testing
    % delegation and messaging within a single instance of Matlab.
    % "Client Delegation" and "Server Compliance" are complementary.  They
    % allow Snow Dots to span more than one instance of Matlab, even on
    % more than one machine.
    % @details
    % dotsAllRemoteObjectManagers itself is not a usable class, it just
    % provides functionality to subclasses (see below).
    % @ingroup foundataion
    
    properties
        % true/false, whether to delegate behaviors to the server
        % counterpart
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        clientMode;
        
        % string IP address to use for client behaviors
        % @details
        % Automatically gets a machine-specific address from
        % dotsTheMachineConfiguration.
        clientIP;
        
        % integer port number to use for client behaviors
        % @details
        % Automatically gets a machine-specific port number from
        % dotsTheMachineConfiguration.
        clientPort;
        
        % socked identifier for doing client behaviors
        % @details
        % Automatically opens a socket with dotsTheMessenger.
        clientSocket;
        
        % true/false, whether to carry out behaviors for the client
        % counterpart
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        serverMode;
        
        % string IP address to use for server behaviors
        % @details
        % Automatically gets a machine-specific address from
        % dotsTheMachineConfiguration.
        serverIP;
        
        % integer port number to use for server behaviors
        % @details
        % Automatically gets a machine-specific port number from
        % dotsTheMachineConfiguration.
        serverPort;
        
        % socked identifier for doing server behaviors
        % @details
        % Automatically opens a socket with dotsTheMessenger.
        serverSocket;
        
        % containers.Map of structs describing delegated behaviors
        % @details
        % Keeps track of all behaviors that a client manager delegates to
        % its server counterpart, including pending and completed
        % behaviors.
        transactions;
        
        % count of delegated behaviors that are not yet carried out
        transactionsPending;
        
        % count of delegated behaviors that were already carried out
        transactionsClosed;
        
        % time to wait while the server counterpart carries out behaviors
        % @details
        % transactionsTimeout should be in units of clockFunction.
        % finishAllTransactions() waits up to this amount of time to allow
        % the server counterpart to finish carrying out delegated
        % behaviors, then gives up.
        transactionsTimeout;
    end
    
    properties (Access = protected)
        % reusable struct to represent transactions with the counterpart
        % @details
        % transactionTemplate has the same fields as a dotsTransaction
        % object.  For performance reasons, its stored as a struct instead
        % of an object.
        transactionTemplate;
        
        % types of transaction to finish as soon as they're opened
        % @details
        % Certain behaviors, like object creation, are time- and
        % order-sensitive.  A client manager will block until its server
        % counterpart has finished carrying them out.
        sychronousTransactionTypes = {'create', 'eliminate', 'join', 'quit'};
        
        % function_handle to choose delegation or local behaviors
        sendTransactionAsClientFunction;
        
        % function_handle to choose compliance or local behaviors
        sendTransactionAsServerFunction;
        
        % the dotsTheMessenger singleton
        theMessenger;
        
        % true/false whether to delegate managed object property changes
        shouldForwardToCounterpart;
        
        % true/false whether to delegate managed object method calls
        shouldCallToCounterpart;
    end
    
    properties (Hidden)
        % number of transaction to delegate during benchmarking
        benchmarkIterations;
        
        % placeholder value used during benchmarking
        benchmarkValue;
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.shouldForwardToCounterpart = false;
            self.shouldCallToCounterpart = false;
            
            self.initialize@dotsAllObjectManagers;

            bogus = 'a';
            self.transactions = containers.Map(bogus, struct, 'uniformValues', false);
            self.transactions.remove(bogus);
            
            dotsTheMachineConfiguration.applySettingsToObjectAsClass( ...
                self, 'dotsAllRemoteObjectManagers');
            dotsTheMachineConfiguration.applySettingsToObject(self);
            
            self.transactionsPending = 0;
            self.transactionsClosed = 0;
            self.transactionTemplate = ...
                dotsTransaction.newTransactionStruct;
            
            self.theMessenger = dotsTheMessenger.theObject;
            self.configureSockets;
            
            self.shouldForwardToCounterpart = true;
            self.shouldCallToCounterpart = true;
        end
        
        % Check for messages at the server socket and carry out behaviors.
        % @param untilNoMessages true/false, whether to keep reading
        % messages and carrying out behaviors until there are no more
        % messages.
        % @details
        % Receives any transaction messages that dotsTheMessenger has
        % ready, at the manager's server socket.  If a message is ready,
        % caries out the behavior specified in the transaction.
        % @details
        % If @a untilNoMessages is true, repeats until dotsTheMessenger has
        % no more messages at the socket.  Default is don't repeat.
        function receiveTransactionsAsServer(self, untilNoMessages)
            sock = self.serverSocket;
            [txn, status] = self.theMessenger.receiveMessageAtSocket(sock);
            if status >= 0
                self.doTransaction(txn);
            end
            
            if nargin == 2 && untilNoMessages
                while status > 0
                    [txn, status] = self.theMessenger.receiveMessageAtSocket(sock);
                    if status >= 0
                        self.doTransaction(txn);
                    end
                end
            end
        end
        
        % Check for messages at the client socket.
        % @param untilNoMessages true/false, whether to keep reading
        % messages until there are no more.
        % @details
        % Receives any transaction messages that dotsTheMessenger has
        % ready, at the manager's client socket.  If a message is ready,
        % acknowledges that the server counterpart has carried out the
        % behavior specified in the transaction.
        % @details
        % If @a untilNoMessages is true, repeats until dotsTheMessenger has
        % no more messages at the socket.  Default is don't repeat.
        function receiveTransactionsAsClient(self, untilNoMessages)
            sock = self.clientSocket;
            [txn, status] = self.theMessenger.receiveMessageAtSocket(sock);
            if status >= 0
                self.acknowledgeTransaction(txn);
            end
            
            if nargin == 2 && untilNoMessages
                while status > 0
                    [txn, status] = self.theMessenger.receiveMessageAtSocket(sock);
                    if status >= 0
                        self.acknowledgeTransaction(txn);
                    end
                end
            end
        end
        
        % Wait until pending transactions are finished.
        % @details
        % The client manager will block until it has acknowledged that its
        % server counterpart has carried out all delegated behaviors.  If
        % transactionsTimeout elapses before all behaviors have been
        % carried out, issues a warning and gives up.
        function finishAllTransactions(self)
            if self.transactionsPending > 0
                endTime = feval(self.clockFunction) + self.transactionsTimeout;
                while self.transactionsPending > 0 && endTime > feval(self.clockFunction)
                    if self.serverMode
                        self.receiveTransactionsAsServer(true);
                    end
                    self.receiveTransactionsAsClient(true);
                end
                
                if self.transactionsPending > 0
                    warning(sprintf('%s did not finish %d transactions', ...
                        class(self), self.transactionsPending));
                end
            end
        end
        
        % Get information about pending, delegated behaviors.
        % @details
        % Returns a struct array of dotsTransaction structs representing
        % behaviors that the client managet has delegated to the server
        % counterpart, but the server counterpart has not yet carried out.
        function txns = getPendingTransactions(self)
            allTxns = self.transactions.values;
            isPending = false(1, length(allTxns));
            for ii = 1:length(allTxns)
                txn = allTxns{ii};
                isPending(ii) = isempty(txn.closeTime);
            end
            txns = [allTxns{isPending}];
        end
        
        % Measure client-server message round trip times
        % @param true/false, whether to open a figure an plot results.
        % @details
        % The client manager sends many transaction messages to its server
        % counterpart and waits to acknowledge that the transaction was
        % carried out.  Returns a struct array containing timing and
        % correctness information from each round trip.  If doPloit is
        % true, also opens a figure and plots the timing information.
        % @details
        % The hidden benchmarkIterations property determines the number of
        % round trips to conduct.  During each iteration, the client
        % forwards a new value to the server's hidden benchmarkValue
        % property, then fetches the same value from the server.  The
        % forwarded and fetched values should always match.
        % @details
        % The returned struct contains two types of timing information:
        % trip time and action time.  Trip time is from the cleint's point
        % of view, how long to took the issue each trancaction and
        % acknowledge that it was completed.  Action time is from the
        % server's point of view, how long it took to carry out the
        % transaction locally.  The difference between trip time and action
        % time indicates messaging overhead.
        function results = benchmarkTransactions(self, doPlot)
            
            results.forwardTripTime = nan(1, self.benchmarkIterations);
            results.forwardActionTime = nan(1, self.benchmarkIterations);
            results.fetchTripTime = nan(1, self.benchmarkIterations);
            results.fetchActionTime = nan(1, self.benchmarkIterations);
            results.valueIsCorrect = false(1, self.benchmarkIterations);
            
            for ii = 1:self.benchmarkIterations
                txn = self.transactionTemplate;
                txn.type = 'forward';
                txn.objectUID = 'self';
                txn.objectProperty = 'benchmarkValue';
                txn.propertyValue = ii;
                forwardTxn = self.openTransaction(txn);
                
                self.finishAllTransactions;
                
                if self.transactionsPending > 0
                    break
                end
                
                % transaction should have been replaced with finished
                % transaction
                txn = self.transactions(forwardTxn.UID);
                % txn = self.transactions.getItemFromGroupWithMnemonic( ...
                %     forwardTxn.type, forwardTxn.UID);
                
                results.forwardTripTime(ii) = txn.closeTime - txn.openTime;
                results.forwardActionTime(ii) = txn.actionEndTime - txn.actionStartTime;
                
                txn = self.transactionTemplate;
                txn.type = 'fetch';
                txn.objectUID = 'self';
                txn.objectProperty = 'benchmarkValue';
                fetchTxn = self.openTransaction(txn);
                
                self.finishAllTransactions;
                
                if self.transactionsPending > 0
                    break
                end
                
                % transaction should have been replaced with finished
                % transaction
                txn = self.transactions(fetchTxn.UID);
                % txn = self.transactions.getItemFromGroupWithMnemonic( ...
                %     fetchTxn.type, fetchTxn.UID);
                
                results.fetchTripTime(ii) = txn.closeTime - txn.openTime;
                results.fetchActionTime(ii) = txn.actionEndTime - txn.actionStartTime;
                results.valueIsCorrect(ii) = txn.propertyValue == forwardTxn.propertyValue;
            end
            
            if nargin > 1 && doPlot
                xaxis = 1:self.benchmarkIterations;
                names = fieldnames(results);
                n = length(names);
                for ii = 1:n
                    sp = subplot(n, 1, ii);
                    data = results.(names{ii});
                    plot(xaxis, data, '.', 'Parent', sp);
                    ylabel(sp, names{ii})
                    m = mean(data);
                    high = .005;
                    if m < high
                        ylim(sp, [0, high])
                        set(sp, 'YTick', [0 m high])
                    else
                        yt = get(sp, 'YTick');
                        set(sp, 'YTick', [0 m max(yt)])
                    end
                end
                modesActive = {'inactive', 'active'};
                t= sprintf('%s: %d transactions, clientMode %s, serverMode %s', ...
                    class(self), self.benchmarkIterations, ...
                    modesActive{1+self.clientMode}, ...
                    modesActive{1+self.serverMode});
                set(gcf, 'NumberTitle', 'off', 'Name', t);
            end
        end
        
        % Set a manager property without reporting it to the counterpart.
        % @param property string name of one of the manager's properties
        % @param value the value to assign to @a property
        % @details
        % setPropertySilently() is like the setPropertySilently() method of
        % the dotsAllManagedObjects superclass.  It allows the manager to
        % treat itseld like a managed object in some cases.
        % @details
        % setPropertySilently() also allows managers also avoides recursion
        % in cases when a manager would normally report a property change
        % to its counterpart.
        function setPropertySilently(self, property, value)
            self.shouldForwardToCounterpart = false;
            self.(property) = value;
            self.shouldForwardToCounterpart = true;
        end
        
        % Managed objects must report property changes here.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of the property of @a object that
        % recently changed.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function acknowledgePropertyChange(self, object, property)
            txn = self.transactionTemplate;
            txn.type = 'forward';
            txn.objectUID = object.UID;
            txn.objectProperty = property;
            txn.propertyValue = object.(property);
            self.openTransaction(txn);
        end
        
        % Managed objects should be created with this method.
        % @param className the string name of a dotsAllManagedObjects
        % subclass
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function object = newObject(self, className)
            object = self.newObject@dotsAllObjectManagers(className);
            txn = self.transactionTemplate;
            txn.type = 'create';
            txn.objectUID = object.UID;
            txn.objectMethod = className;
            self.openTransaction(txn);
        end
        
        % Stop managing an object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function removeObject(self, object)
            self.removeObject@dotsAllObjectManagers(object);
            txn = self.transactionTemplate;
            txn.type = 'eliminate';
            txn.objectUID = object.UID;
            self.openTransaction(txn);
        end
        
        % Add an object to a custom group
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should join.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function addObjectToGroup(self, object, group)
            self.addObjectToGroup@dotsAllObjectManagers(object, group);
            txn = self.transactionTemplate;
            txn.type = 'join';
            txn.objectGroup = group;
            txn.objectUID = object.UID;
            txn.methodArgs = object.UID;
            self.openTransaction(txn);
        end
        
        % Remove an object from a custom group
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should quit.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function removeObjectFromGroup(self, object, group)
            self.removeObjectFromGroup@dotsAllObjectManagers(object, group);
            txn = self.transactionTemplate;
            txn.type = 'quit';
            txn.objectGroup = group;
            txn.objectUID = object.UID;
            self.openTransaction(txn);
        end
        
        % Set one property for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @param value the value to assign to @a property
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setObjectProperty(self, object, property, value)
            self.setObjectProperty@dotsAllObjectManagers(object, property, value);
            txn = self.transactionTemplate;
            txn.type = 'forward';
            txn.objectUID = object.UID;
            txn.objectProperty = property;
            txn.propertyValue = value;
            self.openTransaction(txn);
        end
        
        % Set multiple properties for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param varargin pairs of string property names and values to set
        % to @a object.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setObjectPropertiesMany(self, object, varargin)
            self.setObjectPropertiesMany@dotsAllObjectManagers(object, varargin{:});
            self.callToCounterpart('setPropertiesManyByUID', object.UID, varargin);
        end
        
        % Get one property value from one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @details
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function value = getObjectProperty(self, object, property)
            value = self.getObjectProperty@dotsAllObjectManagers(object, property);
            txn = self.transactionTemplate;
            txn.type = 'fetch';
            txn.objectUID = object.UID;
            txn.objectProperty = property;
            self.openTransaction(txn);
        end
        
        % Set one property for multiple objects.
        % @param group one of the group names in managedObjects
        % @param property the string name of a property shared by all the
        % objects in @a group.
        % @param value the value to assign to @a property
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setGroupProperty(self, group, property, value)
            self.setGroupProperty@dotsAllObjectManagers(group, property, value);
            txn = self.transactionTemplate;
            txn.type = 'forward';
            txn.objectGroup = group;
            txn.objectProperty = property;
            txn.propertyValue = value;
            self.openTransaction(txn);
        end
        
        % Call a method on one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param method the string name of a method of @a object
        % @param varargin optional arguments to pass to @a method.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function callObjectMethod(self, object, method, varargin)
            self.callObjectMethod@dotsAllObjectManagers(object, method, varargin{:});
            txn = self.transactionTemplate;
            txn.type = 'call';
            txn.objectUID = object.UID;
            txn.objectMethod = method;
            txn.methodArgs = varargin;
            self.openTransaction(txn);
        end
        
        % Call a method on one object and assign the return value.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param returnProperty the string name of a property of @a object
        % which will receive the return value from @a method
        % @param method the string name of a method of @a object
        % @param varargin optional arguments to pass to @a method
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function callObjectMethodWithReturn(self, object, returnProperty, method, varargin)
            self.callObjectMethodWithReturn@dotsAllObjectManagers(object, returnProperty, method, varargin{:});
            txn = self.transactionTemplate;
            txn.type = 'call';
            txn.objectUID = object.UID;
            txn.objectProperty = returnProperty;
            txn.objectMethod = method;
            txn.methodArgs = varargin;
            self.openTransaction(txn);
        end
        
        % Call a method on multiple objects.
        % @param group one of the group names in managedObjects
        % @param method the string name of a method shared by all the
        % objects in @a group.
        % @param varargin optional arguments to pass to @a method.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function callGroupMethod(self, group, method, varargin)
            self.callGroupMethod@dotsAllObjectManagers(group, method, varargin{:});
            txn = self.transactionTemplate;
            txn.type = 'call';
            txn.objectGroup = group;
            txn.objectMethod = method;
            txn.methodArgs = varargin;
            self.openTransaction(txn);
        end
    end
    
    methods (Access = protected)
        % Open socket for client or server behavior, or none or both.
        function configureSockets(self)
            if ~self.clientMode && ~self.serverMode
                % make function calls instead of sending messages
                self.sendTransactionAsClientFunction = @doTransaction;
                self.sendTransactionAsServerFunction = @acknowledgeTransaction;
                
            else
                % send transaction messages
                self.sendTransactionAsClientFunction = @sendTransactionAsClient;
                self.sendTransactionAsServerFunction = @sendTransactionAsServer;
                
                self.theMessenger.closeSocket(self.clientSocket);
                self.theMessenger.closeSocket(self.serverSocket);
                if self.clientMode
                    self.clientSocket = self.theMessenger.openSocket( ...
                        self.clientIP, self.clientPort, ...
                        self.serverIP, self.serverPort);
                end
                
                if self.serverMode
                    self.serverSocket = self.theMessenger.openSocket( ...
                        self.serverIP, self.serverPort, ...
                        self.clientIP, self.clientPort);
                end
            end
        end
        
        % Delegate behavior to the server counterpart, or self.
        function sendTransactionAsClient(self, txn)
            self.theMessenger.sendMessageFromSocket(txn, self.clientSocket);
        end
        
        % Report completed behavior to the client counterpart, or self.
        function sendTransactionAsServer(self, txn)
            self.theMessenger.sendMessageFromSocket(txn, self.serverSocket);
        end
        
        % Forward one of the client's own properties to the server.
        function forwardToCounterpart(self, property, value)
            if self.shouldForwardToCounterpart
                txn = self.transactionTemplate;
                txn.type = 'forward';
                txn.objectUID = 'self';
                txn.objectProperty = property;
                txn.propertyValue = value;
                self.openTransaction(txn);
            end
        end
        
        % Delegate one of the client's own method calls to the server.
        function callToCounterpart(self, method, varargin)
            if self.shouldCallToCounterpart
                txn = self.transactionTemplate;
                txn.type = 'call';
                txn.objectUID = 'self';
                txn.objectMethod = method;
                txn.methodArgs = varargin;
                self.openTransaction(txn);
            end
        end
        
        % Delegate one of the client's own method calls to the server.
        function callToCounterpartWithReturn(self, returnProperty, method, varargin)
            if self.shouldCallToCounterpart
                txn = self.transactionTemplate;
                txn.type = 'call';
                txn.objectUID = 'self';
                txn.objectProperty = returnProperty;
                txn.objectMethod = method;
                txn.methodArgs = varargin;
                self.openTransaction(txn);
            end
        end
        
        % Set many properties for the managed object with the given UID.
        function setPropertiesManyByUID(self, UID, args)
            object = self.managedObjects.getItemFromGroupWithMnemonic(self.defaultGroup, UID);
            self.setObjectPropertiesMany(object, args{:});
        end
        
        % Initiate a new transaction to delegate behavior.
        % @param txn a dotsTransaction struct representing a behavior to
        % delegate
        % @details
        % The client manager delegates behaviors to its server counterpart
        % by creating transactions to represend them and opening the
        % transactions with openTransaction().  openTransaction does
        % accunting for the new transaction, sets the the openTime
        % timestamp of @a txn, adds @txn to transactions, and sends it to
        % the server counterpart.
        % @details
        % If the trancation's type is one of the
        % sychronousTransactionTypes, openTransaction() will block until
        % acknowledging that the transaction has been carried out.
        % @details
        % Returns a copy of @a txn.
        function txn = openTransaction(self, txn)
            txn.openTime = feval(self.clockFunction);
            txn.UID = self.getUIDForClassAndTime(txn.type, txn.openTime);
            
            % keep track of this transaction
            self.transactions(txn.UID) = txn;
            self.transactionsPending = self.transactionsPending + 1;
            % disp(sprintf('%s:%s, %s, %s', ...
            %     func2str(self.sendTransactionAsClientFunction), txn.type, ...
            %     txn.objectProperty, txn.objectMethod));
            
            % send off the transaction to self or counterpart
            feval(self.sendTransactionAsClientFunction, self, txn);
            
            % finish certain transactions synchronously
            if any(strcmp(txn.type, self.sychronousTransactionTypes))
                self.finishAllTransactions;
            end
        end
        
        % Process a transaction to carry out a delegated behavior.
        % @param txn a dotsTransaction struct representing a delegated
        % behavior.
        % @details
        % The server manager carries out behaviors delegated by its client
        % counterpart with doTransaction().  doTransaction() coordinates
        % objects, groups, properties, and function calls locally, as
        % specfied in the given @txn.  It sets the transaction's
        % actionStartTime and actionEndTime timestamps, and sends the
        % completed transaction back to the client counterpart.
        function txn = doTransaction(self, txn)
            
            % disp(sprintf('%s:%s, %s, %s', ...
            %     func2str(self.sendTransactionAsServerFunction), txn.type, ...
            %     txn.objectProperty, txn.objectMethod));
            
            txn.actionStartTime = feval(self.clockFunction);
            switch txn.type
                case 'create'
                    if ~self.managedObjects.containsMnemonicInGroup(txn.objectUID, self.defaultGroup)
                        object = self.newObjectHelper(txn.objectMethod, txn.objectUID);
                    end
                    
                case 'eliminate'
                    if self.managedObjects.containsMnemonicInGroup(txn.objectUID, self.defaultGroup)
                        object = self.getObjectForTransaction(txn);
                        for g = object.groups
                            self.removeFromList(object, g{1}, txn.objectUID);
                        end
                    end
                    
                case 'join'
                    object = self.getObjectForTransaction(txn);
                    self.addToList(object, txn.objectGroup, txn.methodArgs);
                    
                case 'quit'
                    if self.managedObjects.containsMnemonicInGroup(txn.objectUID, txn.objectGroup)
                        object = self.getObjectForTransaction(txn);
                        self.removeFromList(object, txn.objectGroup, txn.objectUID);
                    end
                    
                case 'forward'
                    % set property for group or single object
                    if isempty(txn.objectUID)
                        self.setToListGroup(txn.objectGroup, txn.objectProperty, txn.propertyValue);
                    else
                        object = self.getObjectForTransaction(txn);
                        object.setPropertySilently(txn.objectProperty, txn.propertyValue);
                    end
                    
                case 'fetch'
                    % return primitive value with transaction
                    object = self.getObjectForTransaction(txn);
                    txn.propertyValue = object.(txn.objectProperty);
                    
                case 'call'
                    % call method for group or single object
                    args = txn.methodArgs;
                    if isempty(txn.objectUID)
                        self.callToListGroup(txn.objectGroup, txn.objectMethod, args);
                    else
                        object = self.getObjectForTransaction(txn);
                        self.shouldCallToCounterpart = false;
                        if isempty(txn.objectProperty)
                            object.(txn.objectMethod)(args{:});
                        else
                            returnValue = object.(txn.objectMethod)(args{:});
                            object.setPropertySilently(txn.objectProperty, returnValue);
                            txn.propertyValue = returnValue;
                        end
                        self.shouldCallToCounterpart = true;
                    end
            end
            
            % send back the complete transaction
            txn.actionEndTime = feval(self.clockFunction);
            feval(self.sendTransactionAsServerFunction, self, txn);
        end
        
        % Access the object needed to carry out a transaction.
        % @param txn a dotsTransaction struct representing a delegated
        % behavior.
        % @details
        % The server manager stores managed objects that its client
        % counterpart can specify by UID.  getObjectForTransaction()
        % returns the object listed in managedObjects that has the UID
        % specified in the objectUID field of @a txn.
        % @details
        % The objectUID field of @a txn may also contain the string 'self',
        % in which case getObjectForTransaction() returns the server
        % manager itself.
        function object = getObjectForTransaction(self, txn)
            if strcmp(txn.objectUID, 'self')
                object = self;
            else
                object = self.managedObjects.getItemFromGroupWithMnemonic(self.defaultGroup, txn.objectUID);
            end
        end
        
        % Close a tranaction for a delegated, completed behavior.
        % @param txn a dotsTransaction struct representing a previously
        % delegated behavior.
        % @details
        % The client manager acknowledges that its server counter part has
        % carried out a delegated behavior with acknowledgeTransaction().
        % If @a txn contains fetched or returned values, it assigns them to
        % the object and property specified in @a txn.  It does accounting
        % for the finished transaction, sets the closeTime timestamp of
        % @txn and, and replaces the entry in transactions with the
        % now-complete @a txn.
        function acknowledgeTransaction(self, txn)
            if any(strcmp(txn.type, {'fetch', 'call'})) && ~isempty(txn.objectProperty)
                object = self.getObjectForTransaction(txn);
                object.setPropertySilently(txn.objectProperty, txn.propertyValue);
            end
            
            % replace the listed transaction with the closed one
            txn.closeTime = feval(self.clockFunction);
            self.transactions(txn.UID) = txn;
            
            self.transactionsPending = self.transactionsPending - 1;
            self.transactionsClosed = self.transactionsClosed + 1;
        end
    end
end